Phase 4.5 — Settings Refactor and Live-Site Fixes
Arc started 2026-07-10, after Phase 4. Sections are Roman numerals and
steps within them are lettered; each step's title says what it does, followed
by a sentence on what was proposed and, once worked, a few on what was done
and anything learned. Marker legend: ✓ done,
▶ next, ? open
decision, unmarked = planned.
I. Fix live-site crashes and load
- ✓ A. Stop the yioop.com
out-of-memory crashes and CPU pinning. A crawl flood on one
page's wiki history plus spam searches drove repeated out-of-memory
crashes and a pinned core. Bounded the
PackedTableTools
dictionary cache to a byte budget with least-recently-used eviction, took
the markdown and mediawiki parsers linear with
MarkUpScanner::peekLine, clamped each index entry's
NUM_DOCS, stopped a spaces-only list marker spinning
skipBlankLines, and moved history-revision rendering to the
browser. Every fix has tests, holds byte-for-byte against the confirmed
head, and is live.
- ✓ B. Stop the pollett.org mail
server wedging on slow DNS. The single event loop stalled up to
sixty seconds inside a blocking
dns_get_record on inbound
DKIM/SPF/DMARC and outbound mail-exchanger lookups, so one slow sender
froze the daemon. Routed every lookup through a new
AsyncDnsResolver that queries over a non-blocking socket,
yields the loop, and is bounded by MAIL_DNS_TIMEOUT; a unit
test covers name encode and decode, compression pointers, and hand-built
replies.
✓ II. Split Server Settings into two
activities
The one Server Settings screen mixed machine-level configuration with
per-account policy; these steps split it and build out the new
account-policy controls.
- ✓ A. Move the settings field
sets onto a Servers screen and a User, Roles, Groups screen.
Partition the existing field sets between the two activities with no new
fields, as a pure move that keeps every control and locale key working.
Landed as a pure move.
- ✓ B. Add a Role Group Limits
field set with per-role storage and a read path. Give each role
caps on how many groups it may own and how large those groups may grow, an
account's cap being the most generous across its roles. A
ROLE_LIMITS table holds one row per role (-1 means unlimited)
with a version 117 migration, the screen edits every role's caps, and
getUserGroupLimits resolves the maximum across an account's
roles. Enforcement is step E.
- ✓ C. Add a Moderation field
set with an on/off toggle. Gather the moderation constants into
one group and add a switch for whether moderation runs at all. The field
set carries
MODERATION_FLAG_THRESHOLD plus a new
USE_MODERATION, and flagGroupItem only routes a
flagged item to the moderation group when USE_MODERATION is
on.
- ✓ D. Retire GIT_MAX_REPO_SIZE
for a resource-memory cap and add a Developer role. Fold the
standalone git repository-size limit into the per-page resource-memory
limit, and add a role that holds only the Feeds and Wikis activity. The
git push check reads
MAX_PAGE_RESOURCE_MEMORY, and a Developer
role (version 118 migration and Createdb seed) holds Feeds and Wikis with
no modifier, unlike the User role.
- ✓ E. Enforce the role group
limits at the points that create or
grow a group. Check
getUserGroupLimits at group
create, member add, wiki-page add, resource upload, and post, and refuse
past the cap.
Done:
GroupModel gains pure count methods
(countGroupsOwnedByUser, countGroupWikiPages,
countGroupThreads, countThreadPosts, joining the
existing countGroupUsers); all enforcement lives in
SocialComponent so the models stay independent of
RoleModel. Two helpers, overUserGroupLimit and
overGroupLimit, resolve the acting user's or the group
owner's caps through getUserGroupLimits and refuse past a cap
with redirectWithMessage. Group create is held to the acting
user's MAX_GROUPS_OWNED; the wrappers
addGroupItemWithinLimits and
addUserGroupWithinLimits guard the feed and membership adds
for MAX_GROUP_THREADS, MAX_THREAD_POSTS, and
MAX_GROUP_MEMBERS; the new-wiki-page path is held to
MAX_GROUP_WIKI_PAGES; and handleResourceUploads
enforces MAX_THREAD_RESOURCES by file count for thread posts
and MAX_PAGE_RESOURCE_MEMORY by folder bytes for pages. Seven
refusal strings and a GroupModelCountsTest were added.
- ✓ F. Rework the Profile.php
constants onto the live accessor so settings changes skip a
restart. A Profile.php constant is fixed at boot, so a setting
read as
C\NAME needs a restart to change; a
p($label, $value, $set) in Config.php backs each setting with
a per-process cache seeded from the boot constant and overridden whenever
updateProfile rewrites Profile.php, so a read through
C\p('NAME') sees the new value at once. Landed the accessor
(keyed on array_key_exists so a stored zero holds, resolved
against the configs namespace), the updateProfile write hook,
and the updateProfileFields fallback, with a unit test. The
settings reads, including the moderation flags, were then moved to
C\p group by group and verified live.
- ✓ G. Add per-role cost and
charge-frequency billing.
Charge a per-role cost in credits on a frequency (Never, Once, Monthly,
Semi-Annual, Yearly). Done with the sellable-roles work: a role's
ROLE_LIMITS row carries ROLE_COST and
CHARGE_FREQUENCY, both editable in the roles admin panel and
read by the account-page purchase and renewal paths.
Open decisions: ? relabel the current settings
activity display name to “Servers”; ?
whether Git Wiki Pages moves fully to the new activity or keeps a foot on the
Servers screen.
III. Multi-domain hosting and small interface fixes
- ▶ A. Serve seekquarry.com and
frise.com from one Yioop
instance. Get multi-domain handling solid enough that one code
base and index host answers for both domains, each with its own landing
group and branding, rather than separate installs. Planned.
Landing routing unified onto the static controller: a
domain routed to a landing group in the Web Server field set used to
serve that group's Main page through the group controller, while the
Public-group landing and the “Use Wiki Public Main Page”
option both went through the static controller. Both routed cases now
take the static path. The router in src/index.php sends
any positive routed group id to the static controller and passes the
id on a $_SERVER['ROUTE_GROUP_ID'] variable a browser
cannot set, since it is not an HTTP_ header, so only an
admin-set DOMAIN_ROUTE chooses the group.
StaticController gained a staticGroupId()
resolver (the routed id when present and positive, else the
world-readable Public group) and reads every wiki page, header,
footer, alias, and resource from it in place of a hardcoded
PUBLIC_GROUP_ID. Only the bare landing request is
re-homed; other c=static requests still resolve to the
Public group. A StaticControllerTest covers the
resolver, and an end-to-end read confirmed a routed group's Main page
is served while the default falls back to Public.
Routed sub-pages and their self links: serving only the
landing left the group's own links broken, since a link like
[[Bob]] in a routed group's page is baked at save time
as that group's read url on the group controller and, drawn under the
static controller, became a static/<id>?a=wiki…
url with no clean route (an error page). Two changes fix this. The
router now resolves the request host to its landing group for every
static request, not just the bare landing, so a p/Bob
clean url on a routed domain reads that group's Bob page; the host
comes from the admin-set DOMAIN_ROUTE table, so a request
parameter cannot choose the group. And the static controller rewrites
a routed group's baked self links to their p/ form as it
serves the page, body, header, and footer alike. The rewrite keys on
the page_name query the read url ends with rather than on
the exact base url, so it holds whether the page was saved with clean
urls on or off. The rewrite covers a page's own links; cross-group
@@group@page@@ links and [[pages]] index
links are left as they are, and a page missing in the routed group is
not looked up in the Public group. End-to-end on the built-in server:
the routed landing shows its Main with the Bob link drawn as
p/Bob, a static request for that page on the routed host
serves the routed group's copy, and the same request on an unrouted
host still serves the Public group.
- ✓ B. Give the Manage Crawl
start-crawl [+] control a single-line accessible name. Its
accessible name renders as the letters of “Add” stacked on
three lines. Landed a
white-space:nowrap guard on
.icon-glyph, confirmed on the live browser.
- ✓ C. Fix the Add Role form's
vertically-stacked Name
label. The “Name:” label rendered one letter per
line. Removed a stray
min-width:100% on the input cell, the
only one in the views, which was collapsing the label column in Chrome;
verified the fixed form renders on one line.
IV. Other work
Tasks that surfaced while doing the above and stand on their own.
- ✓ A. Restore wiki source,
history, and page-list access. Several related regressions in
wiki viewing. Historical revisions render their resources server-side, a
{{require-signin}} left literal inside
<nowiki> no longer fires, the whole-page sign-in warning
is width-bounded, the Tools “Wiki Pages” link is hidden where
the public group forbids its page list, and an empty
public_source falls back to the group's page-source
setting.
- ✓ B. Move wiki-revision diff
rendering to the browser. A diff built its
longest-common-subsequence table server-side on every crawler visit. The
two revisions now ride to the browser and
renderWikiDiff in
help.js computes the diff there, checked line for line against
the server diff(); this also fixed a latent
array_slice(..., 0) that collapsed any no-common-tail diff to
coarse.
- ✓ C. Instrument mail server
down-events. A silent mail outage left no trace, since the
heartbeat closure never captured the server object and threw every
interval. Fixed that capture and added a shutdown handler that records a
fatal with loop position, memory, and fiber and connection counts;
catchable stop signals are logged, and the resolver logs a lookup no name
server answers in time.
- ✓ D. Build shared test tables
from ProfileModel definitions. Seven model tests stood up shared
tables with hand-copied CREATE TABLE statements that could drift. Each now
calls
ProfileModel::initializeSql and executes the matching
create_statements entry, so the tests track schema changes;
MAIL_SECRET and GIT_APP_CODE, which ProfileModel does not define, keep
their local definitions.
- ✓ E. Keep the admin signed in
when the session cookie name
or CSRF token field name changes. Renaming either on the
Session Parameters panel signed the admin out: the browser kept a
cookie under the old name while the server began expecting the new
one, and a token-field rename dropped the token from the follow-up
redirect. The
renameSessionCookie migration and a
token carry-forward now run on the panel's real save path, the
updatetypes case of security(), just before
updateProfile. An earlier copy of the migration sat in
updateProfileFields, which no SESSION_NAME form reaches,
so it never ran and is removed. Verified live against the secure
server that both renames keep the session.
- ✓ F. Let users buy roles and
subscribe to sellable ones.
A role is put on sale by giving its
ROLE_LIMITS row a
ROLE_COST above zero and a CHARGE_FREQUENCY
other than "never" (both already editable in the roles admin panel).
A new USER_ROLE.EXPIRES column (database version 120,
default FOREVER) records when a bought grant lapses; the
permission queries in UserModel now ignore a grant past
its expiry, so a lapsed role stops conferring its activities. On the
account page, when any role is for sale, a Roles row under the
language setting lists the user's roles, gives each subscribed role an
unsubscribe link, and offers a change-password-style Purchase form that
debits the role's cost from the credit ledger and grants it with an
expiry one billing period out (a "once" purchase never lapses). Viewing
the account also runs a credits-forward pass that spends the balance to
push each lapsed subscription's expiry forward, one period at a time,
for as many periods as the balance covers. Reworking the roles admin
limits screen to fetch only the selected role's row, with an
unsaved-changes confirm on dropdown change, is the immediate
follow-on.
Other work accomplished
- Purchase New Roles now opens the purchase form. The
link carried
edit_purchase=true but not
edit, and the account page draws the purchase form only
in its edit view, so the click landed on the read-only account page
with nothing to buy. edit_purchase=true now opens the
edit view the way edit_pass does, and buying or
unsubscribing returns to that view (the purchase form carries a hidden
edit_purchase, the unsubscribe link carries
edit, and both redirects copy the field forward).
Verified by rendering the account page with a role put up for sale:
the form and the Roles row appear.
- Role Group Limits keeps the role you were editing.
The role drop-down was client-side only, so a save reloaded the panel
on the first role, Admin. The drop-down now submits as
selected_role and marks the chosen role selected, the
save redirect copies selected_role forward, and the
controller hands the view a SELECTED_ROLE it validates
against the loaded roles. Verified by rendering the panel with
Developer chosen: the drop-down comes back on Developer, not
Admin.
- Unsubscribe is now a confirm on the role
name. A subscribed role used to print its name followed by a
separate "unsubscribe" link; the name itself is now the link, and
clicking it raises a browser confirm dialog ("Unsubscribe from this
role?") before following through to the unsubscribe action.
Non-sellable roles stay plain text. Verified by rendering the account
page for a user holding a role put up for sale: the name carries the
confirm and the standalone link is gone.
- Roles label reads "Roles:" and tops the list. The
label gained the colon its sibling rows carry, and the label cell
takes the existing
align-top class so it lines up with
the first line of the roles list rather than floating to the vertical
middle of the two-line cell. Confirmed the label cell has no competing
vertical-align rule, so align-top applies.
- Unsubscribing a role now cancels at period
end. A cancelled sellable role is kept until its paid period
ends but stops renewing (a new WILL_RENEW flag on USER_ROLE, schema
version 121 with its migration), and the account page shows the
expiry date with a resubscribe link that only turns renewing back on
and charges nothing. The account-page settle step removes a cancelled
grant once it lapses, while a grant already lapsed, or a one-time
permanent buy, goes at once. Confirmed by running the component
methods: cancel keeps the role with the flag off, resubscribe
restores it with no charge, a lapsed cancelled grant is removed while
a lapsed renewing grant is pushed forward and charged, and the page
renders the expiry and link; the same model is meant to carry to
group subscriptions, which do not yet exist in the tree.
- Resubscribe is the expiry link, and the cell wraps.
For a cancelled role the whole "expires DATE" text inside the
parentheses is now the link, and clicking it asks "Would you like to
resubscribe?" before resubscribing; the role name and the commas
between roles stay plain text outside any link. The roles cell now
sits in a container capped at the form field width so a long roles
list wraps instead of widening the column and pushing the save button
out of place. Confirmed by rendering both states: the link wraps only
the expiry text with the resubscribe prompt, no comma falls inside a
link, and the cell content sits in the width-capped container.
- pollett.org mail server no longer runs out of
memory on an unterminated SMTP message. The daemon died with
Allowed memory size exhausted during a socket read, memory having
jumped from about 145 MB to a gigabyte in under a minute: each
connection's received bytes accumulate in one buffer until a complete
command or, in the DATA phase, the <CRLF>.<CRLF> marker is
seen, and while the command phase already dropped a line passing
64 KiB, the DATA phase checked the message size only after the
terminator arrived, so a body streamed and never terminated grew
without bound and took every other connection down with it.
consumeSmtpDataPhase now drops the connection once the buffered body
passes MAX_MESSAGE_LEN with no terminator in sight, the way an
over-long command line is already handled; a message that ends within
the limit, or whose terminator lands in the same read that carries it
past the limit, still gets the existing 552 reply. A MailSiteTest
group covers the three outcomes: an unterminated over-limit body
drops the connection, a short partial body keeps waiting, and a
terminated over-limit body still receives 552.
- The wiki parser renders a page's block-class
boxes, resource links, and stacked headings the way the old parser
did. The recursive-descent WikiParser that replaced the old
regex parser had dropped three constructs the frise.org front page is
built from, so that page came out as literal markup, and three fixes
restore the intent without the raw-html passthrough the old nowiki
route allowed. A block-class directive now always emits a
<div class="...">, in a block or inline, where plain class stays
a span; a link splits on its first top-level bar only, so a bar nested
inside a resource, a template, or an inner link is skipped and
[[url| ((resource-nolink:file|alt))]] keeps its href while the
three-field relationship|page|shown form still points at its middle
field; and a heading may run over several lines, an opening = with no
close gathering following lines until one ends in a =, with a blank
line or the end of input leaving the block to ordinary parsing. A
WikiMarkdownTest group covers all three, including the frise.org title
stack, and the existing corpus and markdown suites still pass
unchanged.
- A nowiki example that spans several lines now
stays whole when it starts on a space-indented line. Writing
sample wiki markup in the Wiki Syntax help page surfaced this: a
single leading space marks a line as preformatted, the run was
gathered one indented line at a time, and when a <nowiki> opened
on an indented line but its </nowiki> sat past an unindented
line, the run ended too soon, so the opener was shown as literal
<nowiki> and the rest of the example parsed as ordinary
text. The space-preformatted reader now does what the paragraph reader
already did: while a nowiki span opened inside the run is still open
it keeps taking lines as typed, including unindented ones, until the
span closes, then returns to the leading-space rule. A WikiParserTest
case covers the multi-line case, a single-line nowiki on an indented
line is unchanged, and the parser, corpus, and markdown suites still
pass.
- The Appearance activity now follows the domain
in the address bar, so on a multi-domain instance the admin edits the
look of the domain they are actually viewing. The Customize
Domain picker used to default to the default scope and reload the same
host when changed; it now defaults to the current host when that host
is one of the configured route domains, and changing it moves the
browser to the host serving the chosen scope. Because the two domains
are different registrable names that cannot share a session cookie,
the switch carries a single-use handoff token: the source host mints
it with setSessionUser tied to the signed-in user, and the target
host, in AdminController, redeems it the way a dropped session is
rehydrated from a cookie, then deletes it so it cannot be replayed.
The picker's script, delivered through the controller's script data so
it runs after the page and its base helpers have loaded, marks the
form dirty on any field change and confirms before switching;
SystemComponentTest covers the scope-selection rule, while the live
cross-host handoff needs a multi-domain instance to exercise end to
end.
- The Appearance domain switch now builds its
links the way the rest of the site does, and follows the site's own
url scheme. The picker's switch link and the redirects behind
it were assembled by hand as a query on the site root rather than
through the shared controller-url builder every other link uses, so
they pointed at a different path from the activity's own form; they
now come from that builder. The switch also derived its url scheme
from the site's base-url constant, which in a request is a
host-relative path carrying no scheme at all, so the check always fell
through to https and a plain-http site would have been sent to an
https address that may not answer; the scheme now comes from the
resolver that reports what the request actually arrived over. Checked
against a running server with a routed domain configured: the switch
link renders in the site's clean path form, the switch emits an
absolute http address with its single-use handoff token on a
plain-http site, and saving a colour for a routed domain stores it and
returns to the Appearance form for that domain.
- Each domain a site serves now has its own themes,
and the theme a domain is set to is the one it actually
shows. Themes used to sit loose in the app css folder and be
shared by every host, and a domain's chosen theme was never worn: the
page layout read the theme name from the site-wide setting while only
the domain overlay knew the domain's choice, so a routed domain
dressed itself in the site's theme; each theme now belongs to a scope,
is worn by the domain that chose it, and is kept in a folder of its
own, app/css/<domain> for a
routed domain and app/css/default otherwise, so two domains may each
have a theme of the same name and each domain is offered only its own,
and a version 122 step moves themes already in the css folder into the
default folder while leaving a search.css override where it is.
Adding or editing a theme while a domain was picked saved nothing at
all, as the domain branch returned before the theme was written, and
the editor showed the domain's theme name against the site theme's
rules, so an edit would have saved them over the wrong theme; both are
fixed, and the Appearance form now keeps its values apart from the
fields dressing the page, so it shows the scope the admin picked
rather than the look of the domain they are browsing. Checked against a running server with 127.0.0.1
configured as a route domain: adding a theme there writes it to that
domain's folder, the domain's pages link and serve it, the form shows
the domain's theme on the domain scope and the site's on the default
scope, and rolling a work directory back a version moves its loose
themes into the default folder.
- Deleting a theme works again, and the Appearance
activity stops offering a theme that is not there. Moving
themes into a folder per scope left the delete step looking for them
where they used to be, in the css folder itself, so deleting any
theme reported an error and removed nothing; delete now works in the
scope's own folder, and deleting a domain's theme clears only that
domain's record of it, leaving the domain's other settings and the
site's own theme alone. The activity also offered an edit pencil for a
theme the scope does not have: the theme named by the domain was laid
over the form after the check that the named theme is one of those on
offer, so a name left in a domain's record by the earlier
save-does-nothing bug, when a theme was named but its file never
written, still reached the form and opened an editor with no rules
that could not delete; the check now comes after the domain has had
its say, so a name with no theme behind it leaves the form offering no
theme at all. The Adjust Search Page Components link is gone from the
activity, as search page components are the same for every domain and
the link sitting under a domain picker suggested otherwise; the
setting is still on Page Options, where it reads as the site-wide
choice it is.
- Switching the Appearance domain to another host no
longer signs the admin out. The switch hands the target host
a single-use token naming the signed-in user, and the target restored
that user's session by reading the saved session blob a user carries;
nothing writes that blob at sign-in, so it was always empty, the
restore was skipped, and the token was spent anyway, leaving the admin
at the sign-in page on the domain they had just switched to. The token
is now redeemed by signing that user in under the target host's own
session, the way signing in on that host does, which is all the rest
of the request needed to treat them as signed in, and it is still
spent the moment it is presented so it cannot be offered twice.
Checked against a running server with a name server host and a routed
domain: switching from the default scope to the domain lands signed
in, adding a theme there then works rather than dropping to the
sign-in page, and presenting the same token again from another
browser gets the sign-in page.
- Signed-in sessions survive being dropped, so the
Appearance domain switch no longer ends at the sign-in page.
A session lives in the serving process's memory, and when it is not
there, because the request reached a process without it or the server
had restarted, the code meant to put it back read a saved copy of the
session that is only written when a user changes their settings, so it
was almost always empty and the visitor was sent to sign in; the same
empty read is why the domain switch appeared to work and then dropped
the admin on the next click. Restoring now uses the record of which
user a session belongs to, which every sign-in writes, and puts the
sign-in back under the session id the host is handing the browser
rather than the one the browser arrived with and will never send
again, along with the record of a recent token that a request without
one needs to be given a fresh one; a domain switch also counts as
consent to a session, so a domain the browser holds no cookie for yet
has a session to sign the admin into. Checked against a running server
with a name server host and a routed domain: after dropping the
session, both a request carrying a token and a plain visit to the
admin address stay signed in where both previously went to the
sign-in page, and switching to the domain then adding a theme survives
a dropped session and works from a browser with no cookie for that
domain.
- The Search Toolbar upload moved from Appearance to the
Page Options Search Time tab. A search toolbar is the XML
file a browser reads to offer this site as one of the choices in its
own search box, so it names the site once for a whole browser and has
nothing to do with how a page looks; it was worse than misplaced,
being one of the fields kept per domain, so each domain could hold its
own and the last one saved is what a browser would have been handed.
It is now a setting of the site alongside the other search page
choices, and Appearance uploads images only, which took the file type
test back to a plain check rather than one carrying an exception for
the one file that was not an image; a domain row saved before this
still holds a toolbar, so that field is now passed over when a
domain's look is laid over a page, the way the landing page setting
already was. Checked against a running server: the control is gone
from Appearance and draws on Search Time, uploading an XML file puts
it in the resources folder and names it in the profile, a file that is
not XML is turned away, and a domain still holding an old toolbar gets
the site's toolbar while keeping its own colors.
- The Search Toolbar control reads as part of the Search
Time tab. It had been dropped in among the search page
element checkboxes in a table of its own, so its label was set in a
lighter face than everything around it and it sat under a heading it
had nothing to do with. It now has its own Browser-based Search
heading at the foot of the tab, after Search Results Grouping, with
the control in the same kind of table and the label in the same cell
as Minimum Results to Group directly above it, so the two labels come
out at one size and weight. Rendered the tab headless to check it: the
heading falls between Search Results Grouping and the end of the tab,
and the Search Toolbar label matches the label above it.
- The Browser-based Search heading has a help button of
its own. Every other heading on the Search Time tab carries
one, so the new heading was the only place on the tab offering no
explanation of what its control does. The button points at a
Browser-based Search help page, whose source goes to Chris as a text
file to paste through the wiki UI, since the file holding the help
pages is generated rather than written by hand.
- The server names the request that makes it claim more
memory. The memory sample says how much the server holds but
not what asked for it, and since PHP hands memory back to the system
rarely the figure only ever climbs, so reading it later says nothing
about which request did the climbing. Reading the figure either side of
each request turns that into a line naming the request the moment it
happens, written only when the claim actually grows, which on a server
that has been up a while is never, because by then requests are served
out of memory already claimed. Checked on a running server: the first
request wrote one line naming itself and reporting four megabytes, and
the two requests after it wrote nothing.
- A video's width, height and running time are read once and
kept, rather than by starting ffprobe on every view. These
figures go into a video's page as the tags a link preview reads, and
they only change when the video does, but each view of the page started
an ffprobe process to work them out again; a crawler walking a site's
videos made that a process per hit, which is what the Unable to fork
warnings in the live log are. They now come from a note kept beside the
video's thumbnails, holding the time the video was last changed so a
replaced video is measured again; the test for whether the site has
ffmpeg at all was also asking the wrong question, being true even when
the setting is empty, which had every video view on an install without
ffmpeg starting a shell to run an empty command. Measured against a
real video: reading the figures took 80.7ms the first time and 0.1ms
after that, and changing the video had them read again.
- Themes are one list the whole site shares, and a domain
picks which of them it wears. Giving each domain its own
folder of themes, as the earlier per-domain themes work did, meant a
domain could not wear a theme another domain had made, and a wiki
page's own choice of theme was worse off still: the page settings
offered the themes of the default folder while the page was dressed
from the folder named for the host it was served on, so on a routed
domain a page named a theme that was not where it was looked for and
went undressed. Every theme now lives in the one folder, and all that
a domain, or a wiki page, holds is the name of the theme it wants; a
new database version folds each domain's folder into the shared one,
dropping a copy identical to one already there, and where a name is
taken by different rules it brings the domain's theme over with the
domain's name added to its own and points that domain's record at the
new name, so a domain goes on looking as it did. Checked on a running
server: a theme made for the site is offered while customizing a
domain, where before that list was that domain's own and empty, and
the domain's pages load it; the three cases of the fold were each
exercised against a real database.
- The site has a name, and each domain may go by one of its
own. A page's title and the label on its logo said Yioop
because the words were written into the locale strings themselves, so
a site serving several domains announced itself as Yioop on all of
them and a page came out titled for two sites at once. Site Name is
now an Appearance setting a domain may override like any colour, the
title string takes the name rather than containing it, and a page
that gives itself no title is titled for the site alone instead of
for the site and a separator with nothing after it. Checked on a
running server: the default host titles and labels itself Yioop while
a domain named Frise does so as Frise, and the two locale strings
that had held the names are deleted.
- The site also has a motto, and the two together title a page
that has no title of its own. The words This Search Engine
and This Site had been written into a locale string, which is a
placeholder rather than a translation and named neither the site nor
the domain being served; they are now the Site Motto and Site Name
settings and the string takes them both. Because the profile writes
an empty value for a setting it has never been given, and an empty
value beats the default named in Config, a new site would have come
out with no name at all: the defaults are put in where the profile is
first written, and the Appearance form falls back to them the way it
already did for the logos. Checked on a running server: the default
host titles itself This Search Engine - Yioop while a domain given
its own name and motto uses those.
- The theme dropdown lines up with the buttons beside
it. The dropdown asked for a height of a fixed fraction of
an inch while the buttons take theirs from the glyph they show, so
the two boxes were sized by unrelated rules and only ever matched by
luck; worse, the dropdown had no box-sizing, so the height written
for it was silently not the height it got, the padding and border
being added on top. The row is now laid out as a row and the dropdown
takes whatever height the buttons come out as, which holds wherever
the page is read rather than only where the glyph happens to measure
what it does here.
- A page's resources can be downloaded as one zip.
Anyone wanting a copy of what a page holds had to fetch the files one
at a time and the folders beside them a level at a time, so the
Actions menu over a page's resources now offers Resources ZIP, which
packs the folder and everything below it. A folder larger than the
limit named in Config is refused outright rather than sent short,
since an archive quietly missing files is worse than none, and the
limit is counted in powers of ten so that the size a reader is told
is the size being enforced. The archive is built on disk and read
back a block at a time through the streaming path rather than held
whole the way the mail attachment zip does it, which at this size
would have cost the always-on server a gigabyte it never gives
back.
- The resources download opens as the page keeps
them. It had packed only the resource folder, and packed it
loose under a fixed name, so it unpacked as a heap into whatever
folder it was opened in, carried the folder of earlier versions along
with it, and gave every page the same file. The archive now opens as
one folder holding a resources and a thumbs folder, named after the
page with spaces written as underscores and anything else outside
what a file name may hold url encoded, and the version record is left
out wherever it appears and not counted against the size limit; the
menu item reads Page Resources ZIP and follows Upload File. Checked
by building real archives over a folder with a sub-folder and a
version record: the layout and the names are as expected and none of
the record is in them.
- Deleting a resource returns you to the folder you
deleted it from. Where a reader is sent afterwards is worked
out from the fields the request carried, and the delete link named
the page and the folder but never said it was being used from the
resource listing, so the reader was returned to the page's edit form
instead and had to find their way back. The link now says so, as does
the extract link beside it, which was built the same way and went the
same place.
- A folder dropped onto a page's resources keeps its
folders under a web server too. The folders a dropped file
came from were read off the name the browser gave it, but PHP takes
the folders off that name before a handler ever sees it, so under
Apache every file landed in one heap while under the command line
server, which does no such thinning, the folders survived. The path
is now read from full_path, the field both leave whole and which the
upload already required, so no name has to survive a parser that is
entitled to rewrite it. The sub-path built from it has parent steps
taken out where it is turned into a folder, as before.
- The resources download really is named after its
page. It was meant to be, but the name was being read from a
field the page record does not have, so it was always empty and every
download fell back to the fixed name the fallback exists for, whatever
page it came from. The name of the page being edited was in scope the
whole time and is now what is used. Checked against a real page
record: the field used before is absent from it, and the one used now
gives an archive named for the page.
- The block- forms of id and style work again.
The parser that replaced the old one knew block-class but not
block-id or block-style, so pages written for the old parser had
those directives show as text where a block was meant; they had been
a way of giving a whole region an id to link to or a style to lay it
out by. The three now sit in one list saying which attribute each
sets, so writing class, id or style on its own still wraps text in a
span that keeps flowing, while any of the block- forms wraps it in a
div wherever it is written. Tested for each form, in the middle of a
line as well as alone.
- A media list page can be served as a static HTML
folder. Showing an index page only ever replaced the top
folder, so a folder of generated documentation came out unstyled and
its links dead: everything the index asked for by a path relative to
itself was read as a folder to browse and answered with the wiki page
instead of the file. The setting is now Static HTML Folder, with
Directory Indexes and Index File beside it, and a path beneath such a
page is answered with the file it names, out of that page's resources
and nowhere else, a block at a time; a folder is answered with its
index file, or with a 404 when directory indexes are off. Both the
group url and the static p/ url do this through the same code, so a
domain serves its docs the same way, and a page set up under the old
setting names goes on working.
- Static HTML folders serve the way a web server
serves. The first cut read the two settings as one, so
directory indexes had to be on for a folder to reach its index file
at all, unchecking Static HTML Folder still served as one, a folder
asked for without its closing slash was not found rather than sent to
the slash, and a step back up a path was dropped rather than taken.
Index File now says what stands for a folder and accepts several
names tried in order, while Directory Indexes only says whether a
folder holding none of them shows the page's resource listing or is
not found. The two rows are shown only when the page is a static HTML
folder, since they say nothing otherwise.
- A signed-in reader is no longer sent away from a
static HTML folder. A signed-in reader whose address carried
no token was sent once to the same request with one put on it, which
is harmless for a page but not for a folder of files: the address
landed on is no longer the folder, so every stylesheet, script and
link a file names relative to itself was looked for one level up, and
the file arrived unstyled with dead links. That is why a docs folder
read while signed out was fine and the same folder read while signed
in was not. A read of a static HTML folder is now answered where it
was asked for, while every other page is sent as before.
- Several index file names save as well as serve.
Index File takes a comma separated list tried in order, and serving
already read it that way, but saving cut the whole line back to a
plain file name in one go: any list in which a name carried a slash
kept only what came after the last one and quietly lost every name
before it. Each name is now cut back on its own, as serving does, so
what is saved is what was meant. Checked by serving a folder whose
only index file is the last name in the list and one whose only index
file is the middle name.
- A git repository page can be read at its static url
too. Reading one was only ever set up on the group
controller, so a repository in the Public group could be browsed at
its group url but not at its p/ one, which showed the page's text
rather than the repository. The static controller now sets a
repository page up the same way the group controller does, through the
same code, and the static view shows it with the same element the
group view uses, so a routed domain reaches its repositories this way
as well. The Index File setting is also now labelled Index Files and
says that a comma separated list may be given.
- The commit and tag lists fill in as they are scrolled
again. The call that sets that up was written into the list
itself, but the file it lives in is loaded at the end of the layout,
after the page's own content, so the call ran before the function
existed and the reader saw only the first page of rows. It is now put
where the layout writes it out after that file is loaded, which is
the same place the page's other scripts go. This had been so on the
group url as well as the static one; the static page is only where it
was noticed.
- A repository carrying a ref that names another ref can
be listed again. Every ref file was read as though it held an
object's name, but one can instead hold the name of another ref, as
the file recording which branch a remote's HEAD follows does; its
contents were then handed on as an object name, nothing could be read
under that name, and the listing a client is given died outright,
which reaches the client as an empty reply and stops a clone or a
push before it starts. Such a ref is now passed over, as it is absent
from that listing anyway. Checked against a repository carrying one:
the listing died before and lists the branch after.
- Adding a user gives them their role again.
The row granting a new user the ordinary user role was written by
giving two values in order, but a role grant has since gained two
columns saying when it lapses and whether it renews, and a row given
fewer values than the table has columns is refused outright, so every
user added other than through the account pages ended up holding no
role. The columns are now named, as they already are where the first
accounts are seeded, which lets the two later ones take the defaults
they were added with. This surfaced while importing an issue tracker,
which adds a user per reporter.